import { BaseService } from '@/core/base.service';
import { GeeTestService } from '@/core/geetest.service';
import { NbcbService } from '@/core/nbcb.service';
import {
  BillInfoDto,
  BillListDto,
  BillQueryDto,
  BillRefundDto,
  BillRejectDto,
  BillRollbackDto,
  BillWithdrawBatchDto,
  BillWithdrawDto,
} from '@/dtos';
import { BillStatusEnum, BillTypeEnum, PaymentTypeEnum } from '@/enums';
import { getCode, getNanoid, halt } from '@/utils';
import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { Bill } from '@prisma/client';
import { Queue } from 'bull';
import dayjs from 'dayjs';
import Decimal from 'decimal.js';
import { sleep } from 'radash';

@Injectable()
export class BillService extends BaseService {
  constructor(
    @InjectQueue('bill')
    private readonly billQueue: Queue,
    private readonly nbcbService: NbcbService,
    private readonly geeTestService: GeeTestService,
  ) {
    super();
  }

  async generateOrderNo() {
    return `${dayjs().format('YYYYMMDDHHmmss')}${await getCode(11)}`;
  }

  async getList(ownerId: number, { params, sort }: BillListDto) {
    const { current = 1, pageSize = 20, keyword, ...where } = params;

    let sql = `
      SELECT #{fields} FROM Bill AS t
      WHERE t.isDeleted = false
        AND t.ownerId = ${ownerId}
        ${!!where.type ? `AND t.type = ${where.type}` : ''}
    `;

    const [{ count }] = await this.prisma.$queryRawUnsafe<[{ count: number }]>(
      sql.replace('#{fields}', 'COUNT(t.id) AS count'),
    );

    const list = await this.prisma.$queryRawUnsafe<Bill[]>(
      `${sql.replace('#{fields}', 't.id, t.no, t.type, t.total, t.fee, t.status, t.remark, t.createdAt')}
      ORDER BY ${
        !!sort && Object.keys(sort).length > 0
          ? Object.entries(sort)
              .map(([k, v]) => `t.${k} ${v === 'descend' ? 'DESC' : 'ASC'}`)
              .join(',')
          : 't.createdAt DESC'
      }
      LIMIT ${pageSize} 
      OFFSET ${(current - 1) * pageSize}
      `,
    );

    const total = count ? Number(count) : 0;
    const totalPage = Math.ceil(total / pageSize);
    const hasMore = current < totalPage;

    return {
      total,
      totalPage,
      hasMore,
      current,
      pageSize,
      list: list.map((o) => ({
        ...o,
        total: Number(o.total),
        fee: Number(o.fee),
      })),
    };
  }

  async query({ params, sort }: BillQueryDto) {
    const { current = 1, pageSize = 20, keyword, ...where } = params;

    let sql = `
      SELECT #{fields} FROM Bill AS t
      LEFT JOIN User AS u ON u.id = t.ownerId
      WHERE t.isDeleted = false
        ${!!where.no ? `AND t.no LIKE '%${where.no}%'` : ''}
        ${!!where.serialNo ? `AND t.serialNo LIKE '%${where.serialNo}%'` : ''}
        ${!!where.type ? `AND t.type = ${where.type}` : ''}
        ${!!where.status ? `AND t.status = ${where.status}` : ''}
        ${!!where.mobile ? `AND u.mobile LIKE '%${where.mobile}%'` : ''}
        ${!!where.realname ? `AND u.realname LIKE '%${where.realname}%'` : ''}
        ${!!where.startTime ? `AND CONVERT_TZ(t.createdAt, 'UTC', 'Asia/Shanghai') >= '${where.startTime}'` : ''}
        ${!!where.endTime ? `AND CONVERT_TZ(t.createdAt, 'UTC', 'Asia/Shanghai') <= '${where.endTime}'` : ''}
    `;

    const [{ count }] = await this.prisma.$queryRawUnsafe<[{ count: number }]>(
      sql.replace('#{fields}', 'COUNT(t.id) AS count'),
    );

    const list = await this.prisma.$queryRawUnsafe<any[]>(
      `${sql.replace(
        '#{fields}',
        `
          t.id, t.no, t.serialNo, t.type, t.total, t.fee, t.status, t.createdAt,
          t.extra, t.balance, t.frozen, t.remark, u.mobile, u.realname
        `,
      )}
      ORDER BY ${
        !!sort && Object.keys(sort).length > 0
          ? Object.entries(sort)
              .map(([k, v]) => `t.${k} ${v === 'descend' ? 'DESC' : 'ASC'}`)
              .join(',')
          : 't.createdAt DESC'
      }
      LIMIT ${pageSize} 
      OFFSET ${(current - 1) * pageSize}
      `,
    );

    const total = count ? Number(count) : 0;
    const totalPage = Math.ceil(total / pageSize);
    const hasMore = current < totalPage;

    return {
      total,
      totalPage,
      hasMore,
      current,
      pageSize,
      list: list.map((o) => ({
        ...o,
        total: Number(o.total),
        fee: Number(o.fee),
        balance: Number(o.balance),
        frozen: Number(o.frozen),
      })),
    };
  }

  async refund({ params, sort }: BillRefundDto) {
    const { current = 1, pageSize = 20, keyword, ...where } = params;

    let sql = `
      SELECT #{fields} FROM RefundBill AS t
      WHERE 
        ${!!where.startTime ? `t.createdAt >= '${where.startTime}'` : ''}
        ${!!where.endTime ? `AND t.createdAt <= '${where.endTime}'` : ''}
        ${!!where.serialNo ? `AND t.serialNo LIKE '%${where.serialNo}%'` : ''}
        ${!!where.billNo ? `AND t.billNo LIKE '%${where.billNo}%'` : ''}
        ${!!where.refundCoreNo ? `AND t.serialNo LIKE '%${where.refundCoreNo}%'` : ''}
        ${!!where.rcvName ? `AND t.rcvName = '${where.rcvName}'` : ''}
    `;

    const [{ count }] = await this.prisma.$queryRawUnsafe<[{ count: number }]>(
      sql.replace('#{fields}', 'COUNT(t.id) AS count'),
    );

    const list = await this.prisma.$queryRawUnsafe<any[]>(
      `${sql.replace(
        '#{fields}',
        `
          t.id, t.refundCoreNo, t.refundCoreTime, t.failTime, t.businessCode,
          t.payTime, t.refundMsg, t.rcvName, t.amt, t.billNo, t.payName, t.rcvAcc,
          t.serialNo, t.createdAt
        `,
      )}
      ORDER BY ${
        !!sort && Object.keys(sort).length > 0
          ? Object.entries(sort)
              .map(([k, v]) => `t.${k} ${v === 'descend' ? 'DESC' : 'ASC'}`)
              .join(',')
          : 't.createdAt DESC'
      }
      LIMIT ${pageSize} 
      OFFSET ${(current - 1) * pageSize}
      `,
    );

    const total = count ? Number(count) : 0;
    const totalPage = Math.ceil(total / pageSize);
    const hasMore = current < totalPage;

    return {
      total,
      totalPage,
      hasMore,
      current,
      pageSize,
      list: list.map((o) => ({
        ...o,
      })),
    };
  }

  async getLogByNo(no: string) {
    const log = await this.prisma.bill.findFirst({
      where: {
        no,
        isDeleted: false,
      },
      select: {
        id: true,
        no: true,
        total: true,
        fee: true,
        type: true,
        payType: true,
        payUrl: true,
        status: true,
        remark: true,
        expireAt: true,
        createdAt: true,
        extra: true,
      },
    });
    if (!log) {
      return halt('信息异常');
    }
    return {
      ...log,
      total: Number(log.total),
      fee: Number(log.fee),
    };
  }

  async cancelLogByNo(no: string, ownerId: number) {
    const log = await this.prisma.bill.findUnique({
      where: {
        no,
        ownerId,
        isDeleted: false,
      },
    });
    if (!log) {
      return halt('信息异常');
    }
    await this.billQueue.add('cancel', {
      no: log.no,
    });
    await sleep(2000);
    return true;
  }

  async check({ id }: BillWithdrawDto) {
    const log = await this.prisma.bill.findUnique({
      where: {
        id,
        type: BillTypeEnum.WITHDRAW,
        status: BillStatusEnum.PROCESSING,
        isDeleted: false,
      },
    });
    if (!log) {
      return halt('订单信息异常');
    }

    const res = await this.nbcbService.querySingleTransferResult({
      serialNo: log.serialNo,
    });

    if (!res || res?.Data?.retCode !== '0000') {
      console.log(`核验失败: ${res?.Data?.retMsg ?? '未知错误'}`);
      return halt(`核验失败: ${res?.Data?.retMsg ?? '未知错误'}`);
    }

    const transferDtl = res?.Data?.transferDtl;
    console.log(
      `核验成功:`,
      log.no,
      log.serialNo,
      transferDtl?.status === '0' ? '成功' : transferDtl?.status,
      transferDtl?.errDesc ?? '',
    );

    if (transferDtl?.status === '0') {
      await this.prisma
        .$transaction(async (tx) => {
          const user = await tx.user.findUnique({
            where: {
              id: log.ownerId,
              isDeleted: false,
            },
          });
          if (!user) {
            return halt('用户信息异常');
          }
          await tx.user.update({
            where: {
              id: user.id,
              version: user.version,
            },
            data: {
              version: {
                increment: 1,
              },
              frozen: {
                decrement: Number(log.total),
              },
            },
          });
          await tx.bill.update({
            where: {
              id,
            },
            data: {
              status: BillStatusEnum.COMPLETED,
            },
          });
          return true;
        })
        .catch((e) => {
          console.log('操作失败', e);
          return halt('操作失败');
        });
    } else if (
      ['-2', '-3', '2', '4', '5', '6', '96', '112'].includes(
        transferDtl?.status,
      )
    ) {
      await this.prisma
        .$transaction(async (tx) => {
          const user = await tx.user.findUnique({
            where: {
              id: log.ownerId,
              isDeleted: false,
            },
          });
          if (!user) {
            return halt('用户信息异常');
          }
          await tx.user.update({
            where: {
              id: user.id,
              version: user.version,
            },
            data: {
              version: {
                increment: 1,
              },
              balance: {
                increment: Number(log.total),
              },
              frozen: {
                decrement: Number(log.total),
              },
            },
          });
          await tx.bill.update({
            where: {
              id,
            },
            data: {
              status: BillStatusEnum.REJECTED,
              remark: transferDtl?.errDesc ?? '未知错误',
            },
          });
          try {
            await this.geeTestService.sendRefund(user.mobile);
          } finally {
            return true;
          }
        })
        .catch(() => {
          return halt('操作失败');
        });
    } else {
    }
    return transferDtl;
  }

  async checkBatch() {
    const logs = await this.prisma.bill.findMany({
      where: {
        type: BillTypeEnum.WITHDRAW,
        status: BillStatusEnum.PROCESSING,
        isDeleted: false,
      },
      take: 2000,
      orderBy: {
        createdAt: 'asc',
      },
    });
    for (let i = 0; i < logs.length; i++) {
      const log = logs[i];
      console.log('核验:', log.id, log.no, log.serialNo);
      try {
        await sleep(1000);
        await this.check({ id: log.id });
      } catch (e) {
        console.error(e);
      }
    }
  }

  async withdraw({ id }: BillWithdrawDto) {
    return await this.redlock.using(
      [`bill:withdraw:${id}`],
      10000,
      {},
      async () => {
        const log = await this.prisma.bill.findUnique({
          where: {
            id,
            type: BillTypeEnum.WITHDRAW,
            status: BillStatusEnum.PENDING,
            isDeleted: false,
          },
        });
        if (!log) {
          return halt('订单信息异常');
        }

        const user = await this.prisma.user.findUnique({
          where: {
            id: log.ownerId,
            isDeleted: false,
          },
        });

        if (!user) {
          return halt('用户信息异常');
        }

        if (!user.isReal) {
          return halt('请先完成实名认证');
        }

        const amount = new Decimal(log.total)
          .sub(new Decimal(log.fee))
          .toDecimalPlaces(2)
          .toNumber();
        const bank: any = log.extra;
        let serialNo = (await getCode(4)) + (await getNanoid(4));

        if (this.config.get('payment') === PaymentTypeEnum.UOP) {
          serialNo = log.serialNo; // await this.nbcbService.generateSerialNo();
          try {
            const params = {
              serialNo,
              rcvName: user.realname,
              rcvAcc: bank.bankNo,
              rcvBankNo: bank.bankCode,
              purpose: `提现: ¥${new Decimal(log.total).toDecimalPlaces(2).toNumber()}，手续费: ¥${new Decimal(log.fee).toDecimalPlaces(2).toNumber()}`,
              amt: amount,
            };
            const res = await this.nbcbService.singleTransfer(params);
            console.log('宁波银行申请提现', params, res);
            if (!res || res?.Data?.retCode !== '0000') {
              return halt(`申请提现失败: ${res?.Data?.retMsg ?? '未知错误'}`);
            }
          } catch (e) {
            console.log(e);
            return halt('申请提现失败');
          }
        }

        return await this.prisma
          .$transaction(async (tx) => {
            const user = await tx.user.findUnique({
              where: {
                id: log.ownerId,
                isDeleted: false,
              },
            });
            if (!user) {
              return halt('用户信息异常');
            }
            await tx.bill.update({
              where: {
                id,
              },
              data: {
                status: BillStatusEnum.PROCESSING,
                serialNo,
              },
            });
            return true;
          })
          .catch(() => {
            return halt('操作失败');
          });
      },
    );
  }

  async withdrawBatch({ startTime, endTime }: BillWithdrawBatchDto) {
    console.log('开始批量出账', startTime, endTime);
    // const logs = await this.prisma.bill.findMany({
    //   where: {
    //     type: BillTypeEnum.WITHDRAW,
    //     status: BillStatusEnum.PENDING,
    //     isDeleted: false,
    //     date: {
    //       gte: dayjs(startTime).toDate(),
    //       lte: dayjs(endTime).toDate(),
    //     },
    //   },
    //   select: {
    //     id: true,
    //     no: true,
    //     serialNo: true,
    //     owner: true,
    //     total: true,
    //   },
    //   take: 2000,
    //   orderBy: {
    //     createdAt: 'asc',
    //   },
    // });
    const logs = await this.prisma.$queryRawUnsafe<any>(`
      SELECT t.id, t.no, t.serialNo, t.ownerId, t.total 
      FROM Bill AS t
      WHERE t.type = ${BillTypeEnum.WITHDRAW}
        AND t.status = ${BillStatusEnum.PENDING}
        AND t.isDeleted = false
        AND t.date = '${dayjs(startTime).format('YYYY-MM-DD')}'
      ORDER BY t.createdAt ASC
      LIMIT 2000
    `);

    for (let log of logs) {
      console.log(
        '出账:',
        log.id,
        log.no,
        log.serialNo,
        log.total,
        log.ownerId,
      );
      try {
        await sleep(1000);
        await this.withdraw({ id: log.id });
      } catch (e) {
        console.error(e);
      }
    }
  }

  async reject({ id, remark }: BillRejectDto) {
    return await this.redlock.using(
      [`bill:reject:${id}`],
      10000,
      {},
      async () => {
        const log = await this.prisma.bill.findUnique({
          where: {
            id,
            type: BillTypeEnum.WITHDRAW,
            status: BillStatusEnum.PENDING,
            isDeleted: false,
          },
        });
        if (!log) {
          return halt('订单信息异常');
        }
        return await this.prisma
          .$transaction(async (tx) => {
            const user = await tx.user.findUnique({
              where: {
                id: log.ownerId,
                isDeleted: false,
              },
            });
            if (!user) {
              return halt('用户信息异常');
            }
            await tx.user.update({
              where: {
                id: user.id,
                version: user.version,
              },
              data: {
                version: {
                  increment: 1,
                },
                balance: {
                  increment: Number(log.total),
                },
                frozen: {
                  decrement: Number(log.total),
                },
              },
            });
            await tx.bill.update({
              where: {
                id,
              },
              data: {
                status: BillStatusEnum.REJECTED,
                remark,
              },
            });
            try {
              await this.geeTestService.sendRefund(user.mobile);
            } finally {
              return true;
            }
          })
          .catch(() => {
            return halt('操作失败');
          });
      },
    );
  }

  async info({ id }: BillInfoDto) {
    const log = await this.prisma.bill.findUnique({
      where: {
        id,
        type: BillTypeEnum.WITHDRAW,
        isDeleted: false,
      },
    });
    if (!log) {
      return halt('订单信息异常');
    }
    return await this.nbcbService.querySingleTransferResult({
      serialNo: log.serialNo,
    });
  }

  async rollback({ id, remark }: BillRollbackDto) {
    return await this.redlock.using(
      [`bill:rollback:${id}`],
      10000,
      {},
      async () => {
        const log = await this.prisma.bill.findUnique({
          where: {
            id,
            type: BillTypeEnum.WITHDRAW,
            status: BillStatusEnum.COMPLETED,
            isDeleted: false,
          },
        });
        if (!log) {
          return halt('订单信息异常');
        }
        return await this.prisma
          .$transaction(async (tx) => {
            const user = await tx.user.findUnique({
              where: {
                id: log.ownerId,
                isDeleted: false,
              },
            });
            if (!user) {
              return halt('用户信息异常');
            }
            await tx.user.update({
              where: {
                id: user.id,
                version: user.version,
              },
              data: {
                version: {
                  increment: 1,
                },
                balance: {
                  increment: Number(log.total),
                },
              },
            });
            await tx.bill.update({
              where: {
                id,
              },
              data: {
                status: BillStatusEnum.REJECTED,
                remark,
              },
            });
            try {
              await this.geeTestService.sendRefund(user.mobile);
            } finally {
              return true;
            }
          })
          .catch(() => {
            return halt('操作失败');
          });
      },
    );
  }
}
